/*
 * File     : StructureBuilderSession.java
 * Created  : 15 Jan 2012
 *
 * Copyright © 2011 Matthew Wilson (mj. {my-surname} .uk {at} gmail.com)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.googlecode.dni.internal.structure;

import static com.googlecode.dni.internal.DniInternal.LOGGER;

import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;

import com.googlecode.dni.internal.DniInternal;
import com.googlecode.dni.type.structure.Structure;

/**
 * <p>
 *  Manages the creation of structures.
 * </p>
 *
 * @author Matthew Wilson
 */
public final class StructureManager
{

    /**
     * Special session that's used when there's no actual session in place;
     * this is used when building structure-like methods for a library.
     */
    static final Session NO_SESSION = new NoSession();

    private static final Map< Class< ? extends Structure >, StructureRecord > RECORDS =
        new WeakHashMap< Class< ? extends Structure >, StructureRecord >();

    private static boolean creating;

    /**
     * <p>
     *  Obtains or creates if missing the structure record for the given type.
     * </p>
     * <p>
     *  Requires some kind of external synchronization.
     * </p>
     *
     * @param <T>
     *            the structure type
     * @param type
     *            the structure type
     * @return the structure record
     */
    public static < T extends Structure > StructureRecord get( final Class< T > type )
    {
        StructureRecord record = RECORDS.get( type );
        if ( record != null )
        {
            return record;
        }

        if ( creating )
        {
            throw new IllegalStateException( "Recursive calls not permited" );
        }

        creating = true;
        try
        {
            DefaultSession session = new DefaultSession();
            record = session.create( type );
            session.complete();

            RECORDS.put( type, record );
        }
        finally
        {
            creating = false;
        }

        return record;
    }

    /**
     * Clears all structure records.  For use only in testing.
     */
    public static void clear()
    {
        if ( DniInternal.TESTING == null )
        {
            throw new AssertionError();
        }

        RECORDS.clear();
    }

    /**
     * <p>
     *  Encapsulates behaviour relating to the creation of a group of linked
     *  structures.
     * </p>
     */
    interface Session
    {
        /**
         * <p>
         *  Obtains or creates if missing the structure record for the given
         *  type.
         * </p>
         * <p>
         *  There is no guarantee that the type will have been initialised.
         *  This method is only for use by references that need the complete
         *  type information, and hence does not permit recursive references.
         * </p>
         *
         * @param <T>
         *            the structure type
         * @param type
         *            the structure type
         * @return the structure record
         */
        < T extends Structure > StructureRecord getOrCreate( Class< T > type );

        /**
         * <p>
         *  Obtains the names for the given type.
         * </p>
         * <p>
         *  If the type has not been created, its creation will be scheduled.
         * </p>
         *
         * @param <T>
         *            the structure type
         * @param type
         *            the structure type
         * @return the structure record
         */
        < T extends Structure > StructureTypeNames getNames( Class< T > type );
    }

    /**
     * <p>
     *  Encapsulates behaviour relating to the creation of a group of linked
     *  structures.
     * </p>
     * <p>
     *  This is moderately complicated.  There are three stages:
     * </p>
     * <ol>
     *  <li>Creating implementation types</li>
     *  <li>Loading classes</li>
     *  <li>Initialising classes</li>
     * </ol>
     * <p>
     *  TODO v1.0 finish documenting how it works
     * </p>
     *
     * @author Matthew Wilson
     */
    private static final class DefaultSession implements Session
    {
        private final Map< Class< ? extends Structure >, StructureEntry > entries = new HashMap< Class< ? extends Structure >, StructureEntry >();

        @Override
        public < T extends Structure > StructureRecord getOrCreate( final Class< T > type )
        {
            // might have been created before
            StructureRecord record = RECORDS.get( type );
            if ( record != null )
            {
                return record;
            }

            // might have been created in this session
            StructureEntry entry = this.entries.get( type );

            if ( entry == null )
            {
                entry = createEntry( type );
            }

            if ( entry.record == null )
            {
                // OK, it's scheduled; let's do it now
                entry.build();
            }

            return entry.record;
        }

        @Override
        public < T extends Structure > StructureTypeNames getNames( final Class< T > type )
        {
            // might have been created before
            StructureRecord record = RECORDS.get( type );
            if ( record != null )
            {
                return new StructureTypeNames( record.getImplName(),
                                               record.getFactoryImplName() );
            }

            // might have been created in this session
            StructureEntry entry = this.entries.get( type );
            if ( entry != null )
            {
                return entry.names;
            }

            // OK, schedule it
            entry = createEntry( type );
            return entry.names;
        }

        private < T extends Structure > StructureRecord create( final Class< T > type )
        {
            StructureEntry entry = createEntry( type );

            entry.build();

            return entry.record;
        }

        private < T extends Structure > StructureEntry createEntry( final Class< T > type )
        {
            StructureTypeNames names = getStructureImplNames( type );
            StructureBuilder< ? > builder = StructureBuilder.create( this,
                                                                     type,
                                                                     names.implName,
                                                                     names.factoryImplName );

            StructureEntry entry = new StructureEntry( type, builder, names );
            StructureEntry old = this.entries.put( type, entry );
            assert old == null;

            return entry;
        }

        private void complete()
        {
            // TODO v1.0 catch and rethrow exceptions to give more context

            // build any that have been deferred
            boolean allBuilt;
            do
            {
                allBuilt = true;
                inner: for ( StructureEntry entry : this.entries.values() )
                {
                    if ( entry.record == null )
                    {
                        entry.build();
                        allBuilt = false;
                        break inner;
                    }
                }
            } while ( !allBuilt );

            // load all the classes
            for ( StructureEntry entry : this.entries.values() )
            {
                entry.builder.loadClass( entry.record );
            }

            // perform class initialisation for all implementation types;
            // this also involves connecting any native methods
            for ( StructureEntry entry : this.entries.values() )
            {
                entry.builder.initClass( entry.record );
            }

            // publish the classes
            for ( StructureEntry entry : this.entries.values() )
            {
                StructureRecord old = RECORDS.put( entry.type, entry.record );
                assert old == null;
            }
        }

        /**
         * <p>
         *  Determines the names of the structure implementation types.  The names
         *  are stored for later use.
         * </p>
         *
         * @param type
         *            the structure interface type
         * @return the implementation type names (byte-code style)
         */
        private static StructureTypeNames getStructureImplNames( final Class< ? extends Structure > type )
        {
            String baseName = type.getName().replace( '.', '/' );
            ClassLoader classLoader = type.getClassLoader();

            // TODO v1.1 With carefully crafted interface names, clashes could be engineered.
            // Keep a map of reserved-but-not-yet-loaded names.
            String implName = DniInternal.getUnusedClassName( baseName + "_Impl_", classLoader );
            String factoryImplName = DniInternal.getUnusedClassName( baseName + "_FactoryImpl_", type.getClassLoader() );

            return new StructureTypeNames( implName, factoryImplName );
        }

        /**
         * Record of the creation state.
         */
        private static final class StructureEntry
        {
            final Class< ? extends Structure > type;
            final StructureBuilder< ? > builder;
            final StructureTypeNames names;
            StructureRecord record;
            private boolean building;

            StructureEntry( final Class< ? extends Structure > type,
                            final StructureBuilder< ? > builder,
                            final StructureTypeNames names )
            {
                this.type = type;
                this.builder = builder;
                this.names = names;
            }

            void build()
            {
                if ( this.building )
                {
                    throw new RecursiveStructureException( this.type.getName() );
                }

                LOGGER.info( "Creating Structure implementation for: " + this.type.getName() );

                this.building = true;
                try
                {
                    this.record = this.builder.build();
                }
                finally
                {
                    this.building = false;
                }
            }
        }
    }

    private static final class NoSession implements Session
    {
        @Override
        public < T extends Structure > StructureRecord getOrCreate( final Class< T > type )
        {
            return DniInternal.getStructureRecord( type );
        }

        @Override
        public < T extends Structure > StructureTypeNames getNames( final Class< T > type )
        {
            StructureRecord record = DniInternal.getStructureRecord( type );
            return new StructureTypeNames( record.getImplName(),
                                           record.getFactoryImplName() );
        }
    }

    /**
     * Holds type names for a structure implementation.
     *
     * @author Matthew Wilson
     */
    static final class StructureTypeNames
    {
        private final String implName;

        private final String factoryImplName;

        /**
         * @param implName
         *            the implementation type name
         * @param factoryImplName
         *            the factory type name
         */
        private StructureTypeNames( final String implName, final String factoryImplName )
        {
            this.implName = implName;
            this.factoryImplName = factoryImplName;
        }

        /**
         * @return the implementation type name (byte-code style)
         */
        public String getImplName()
        {
            return this.implName;
        }

        /**
         * @return the structure factory name (byte-code style)
         */
        public String getFactoryImplName()
        {
            return this.factoryImplName;
        }
    }

}
